# 域名解析 dns
30 秒速记
- 两套解析:
dns.lookup()走操作系统(读hosts,和ping一样);dns.resolve*()直接发DNS查询 lookup底层是getaddrinfo,跑在libuv线程池里(默认4个线程),解析慢会堵住fs、cryptohttp.get、net.connect默认用的都是lookup- 一个域名可能有多个
IPv4/IPv6地址,要处理顺序和失败回退 - 解析结果会变:缓存要按
TTL失效;做SSRF防护要校验最终连接的IP
Node 的 dns 模块其实是两套东西:dns.lookup() 让操作系统帮你查,dns.resolve 系列自己去问 DNS 服务器。 lookup 的结果和你在终端里 ping 一个域名看到的一致,会读 /etc/hosts,http、net 这些模块发请求默认也是用它;但它底层调的是同步的 getaddrinfo,Node 只能把它扔进 libuv 线程池,默认只有 4 个线程,DNS 一慢就会把同池子的文件读写、加密操作一起拖慢。resolve4、resolveMx 这些走 c-ares 做真正的网络查询,不读 hosts,也不占线程池,还能拿到 TTL。
理解「域名解析 dns」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
lookup 和 resolve 的差别,归根结底是「谁在查」:
const dns = require('dns');
// 1. 交给操作系统查:读 hosts、遵循系统解析配置,跑在 libuv 线程池
dns.lookup('localhost', (err, address, family) => {
console.log('lookup:', address, family); // lookup: 127.0.0.1 4(或 ::1 6)
});
// 2. 自己发 DNS 报文查:不读 hosts,走 c-ares,不占线程池
dns.resolve4('example.com', { ttl: true }, (err, records) => {
console.log('resolve4:', records); // [{ address: '93.x.x.x', ttl: 3600 }]
});
// 3. Promise 版本,现在写新代码推荐用这个
dns.promises.lookup('example.com').then(console.log);
几条要记住的事实:
http.request、net.connect、fetch默认都走dns.lookup,所以hosts里配的映射对它们生效。Node 17起lookup默认按系统返回的原始顺序给结果(verbatim),之前的版本会把IPv4排在前面;可以用dns.setDefaultResultOrder('ipv4first')改回来。Node 20起net.connect默认开启autoSelectFamily,会对IPv4和IPv6地址做类似Happy Eyeballs的竞速,IPv6不通时能更快回退。lookup本身没有缓存,也拿不到TTL;需要缓存就自己做,按TTL过期。
面试官追问
追问 1线上 DNS 偶尔慢,结果读文件、bcrypt 加密也跟着变慢,为什么?
dns.lookup 和 fs、crypto 共用 libuv 线程池,默认 4 个线程,几个慢解析就把池子占满了。可以调大 UV_THREADPOOL_SIZE,或者在请求里用自定义 lookup 加缓存,热点域名别每次都现查。
追问 2axios 请求内部服务,每次都要解析一遍域名,要不要做 DNS 缓存?
高频调用值得做,但要按 TTL 过期,别永久缓存,否则服务迁移换 IP 之后一直打旧地址。可以用 cacheable-lookup 这类库挂到 http.Agent 上。
追问 3做 SSRF 防护,先 lookup 校验不是内网 IP 再发请求,够吗?
不够,校验和真正连接时各解析一次,攻击者可以用 DNS 重绑定让第二次解析变成 127.0.0.1。要么把校验后的 IP 直接用来连接,要么在自定义 lookup 里校验,保证检查的就是实际连的那个地址。
追问 4测试里想断言 www.example.com 解析到某个固定 IP,靠谱吗?
不靠谱,公网域名的 IP 随时会变,还受 CDN 调度影响。断言返回格式和地址族就够了;需要确定结果时,用 hosts 或者 mock 掉 dns.lookup。
# 域名解析:dns.lookup()
30 秒速记
dns.lookup(host, cb)回调是(err, address, family),默认只给一个地址- 传
{ all: true }拿全部地址,回调第二个参数变成数组[{ address, family }] family是数字4或6;也可以传{ family: 4 }只要IPv4- 默认结果顺序:
Node 17起按系统返回顺序,之前是IPv4优先 - 解析成功不代表连得上,解析失败和连接失败要分开记
dns.lookup() 就是问操作系统「这个域名对应哪个 IP」,默认只回一个,想要全部就传 { all: true }。 默认回调签名是 (err, address, family),address 是字符串,family 是 4 或 6;加了 all: true 之后第二个参数变成数组,每项是 { address, family },family 参数就没了。所以同一个域名查出来只有一个 IP,不代表它真的只有一个,只是默认模式挑了第一个给你。还有个版本差异要知道:Node 17 之前会把 IPv4 排在前面,之后按系统给的原始顺序,有些机器上就会先拿到 IPv6。
比如我们要查询域名 www.qq.com 对应的ip,可以通过 dns.lookup() 。
var dns = require('dns');
dns.lookup('www.qq.com', function(err, address, family){
if(err) throw err;
console.log('例子A: ' + address);
});
输出如下:
例子A: 182.254.34.74
我们知道,同一个域名,可能对应多个不同的ip。那么,如何获取一个域名对应的多个ip呢?可以这样。
var dns = require('dns');
var options = {all: true};
dns.lookup('www.qq.com', options, function(err, address, family){
if(err) throw err;
console.log('例子B: ' + address);
});
输出如下:
例子B: [{"address":"182.254.34.74","family":4},{"address":"240e:e1:8100:28::2:16","family":6}]
面试官追问
追问 1默认 lookup 只打出一个 IP,能说明这个域名只有一个地址吗?
不能,默认模式只返回第一个。加 { all: true } 才能看到全部,比如同时有 IPv4 和 IPv6 地址。
追问 2升级到 Node 18 之后,有些请求开始报 ECONNREFUSED ::1,怎么回事?
Node 17 起 lookup 不再强制 IPv4 优先,localhost 先解析成了 ::1,而服务只监听了 127.0.0.1。要么服务监听 :: 或同时监听两种地址,要么连接时写 127.0.0.1,或者调用 dns.setDefaultResultOrder('ipv4first')。
追问 3传了 { all: true } 后还用 address.split('.') 处理,会出什么事?
这时 address 是对象数组,不是字符串,直接报 split is not a function。两种模式的回调形态不同,封装时要统一成数组再往下传。
追问 4机器只有 IPv4 出口,怎么让 lookup 只返回 IPv4?
传 { family: 4 },dns.lookup(host, { family: 4 }, cb)。http.request 也支持同名的 family 选项,会透传给 lookup。
追问 5dns.lookup 有 Promise 写法吗?
有,const { address } = await require('dns').promises.lookup(host),Node 15 起也可以 require('dns/promises')。传
